/**
 * 程序功能：使用MQTT协议，与阿里云物联网平台进行数据通信。
 *          在本示例程序中，实现了自动OTA远程升级，适用于无人值守的设备，自动化执行程序固件升级。
 * 使用教程：https://blog.yyzt.site/
 * 注意事项：1.所连接的WIFI名称和WIFI需要设置正确，并且仅支持连接2.4GHz的WIFI，不支持5GHz的WIFI
 *          2.阿里云物联网平台的设备证书（也叫三元组，ProductKey、DeviceName、DeviceSecret）需要设置正确
 *          3.相关属性名称标识符和数据类型需要设置正确
 * 参考资料：https://github.com/0xYootou/arduino-aliyun-iot-sdk
 *          https://github.com/legenddcr/aliyun-mqtt-arduino
 *          https://help.aliyun.com/document_detail/89301.html
 * 阿里云物联网平台网址：https://iot.console.aliyun.com/lk/summary/new
 * OTA升级文档：https://help.aliyun.com/document_detail/89307.html
 */

#include <Arduino.h>            // 导入Arduino库文件
#include <ESP8266httpUpdate.h>  // 导入ESP8266httpUpdate库文件
#include <YYZT_AliyunMqtt.h>    // 导入YYZT_AliyunMqtt库文件

/**
 * 串口调试打印相关参数
 */
#define debugState true             // 串口调试打印状态，true--开启调试打印，false--关闭调试打印
#define debugSerial Serial          // 调试打印的串口
#define debugSerialBuadrate 115200  // 调试打印的串口波特率

/**
 * 阿里云物联网平台相关参数
 */
#define productKey "a17RF1Mnqss"                         // 产品密钥 ProductKey
#define deviceName "ESP8266_001"                         // 设备名称 DeviceName
#define deviceSecret "7bb4991a4beabf6a14918a76819cd3bc"  // 设备密钥 DeviceSecret

String deviceReportFwUpgradeInfoTopic = "/ota/device/inform/" + String(productKey) + "/" + String(deviceName);        // 设备上报固件升级信息Topic
String fwUpgradeInfoDownlinkTopic = "/ota/device/upgrade/" + String(productKey) + "/" + String(deviceName);           // 固件升级信息下行Topic
String deviceReportFwUpgradeProgressTopic = "/ota/device/progress/" + String(productKey) + "/" + String(deviceName);  // 设备上报固件升级进度Topic

/**
 * WIFI相关参数
 */
#define wifiName "YYZT_2.4G"       // WIFI名称
#define wifiPassword "1234567890"  // WIFI密码

/**
 * MQTT相关参数
 */
#define mqttHeartbeatTime 60        // 心跳间隔时间，默认为60s，单位为：秒
#define mqttPacketSize 1024         // MQTT数据包的大小设置最大为1024字节
#define ckeckMqttConnectTime 10000  // 检查MQTT连接的间隔时间，默认为10秒
AliyunMqtt aliyunMqtt;              // 实例化aliyunMqtt对象

// MQTT的连接状态
typedef enum {
  MqttDisconnected = 0,  // MQTT已断开连接
  MqttConnecting,        // MQTT连接中
  MqttConnected          // MQTT已连接
} MqttConnectState;
uint8_t nowMqttConnectState = MqttDisconnected;  // 当前MQTT的连接状态，初始化为MqttDisconnected

/**
 * 程序版本相关参数
 */
#define softwareVersion "1.0.0"  // 程序版本
bool fwUpgradeFlag = false;      // 固件升级的标志，默认为false
String fwDownloadUrl = "";       // 固件下载链接

/**
 * 函数功能：程序升级开始的回调函数
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void updateStartedCallback() {
#if debugState
  debugSerial.println("程序开始升级......");
#endif
}

/**
 * 函数功能：程序升级过程中的回调函数
 * 参数1：[_currentFirmwareSize] [int] 当前已完成下载的固件大小
 * 参数2：[_totalFirmwareSize] [int] 总的固件大小
 * 返回值：无
 * 注意事项：无
 */
void updateProgressCallback(int _currentFirmwareSize, int _totalFirmwareSize) {
  int8_t nowProgress = map(_currentFirmwareSize, 0, _totalFirmwareSize, 1, 100);  // 计算升级进度

#if debugState
  debugSerial.println("正在更新固件: 总大小 " + String(_totalFirmwareSize) + " Bytes，已更新 " + String(_currentFirmwareSize) + " Bytes，进度：" + String(nowProgress) + "%");
#endif

  static uint32_t t = 0;                                               // 记录当前时间
  static bool progressFlag = false;                                    // 记录进度100的标志
  if ((millis() - t > 3000 || nowProgress >= 100) && !progressFlag) {  // 3秒或者当进度到达100时上报1次，并且进度条100标志位为false
    if (nowProgress >= 100) {                                          // 如果进度达到100
      progressFlag = true;                                             // 进度条100标志位为true
    }
    t = millis();                                                                           // 更新时间
    StaticJsonDocument<64> jsonData;                                                        // 实例化一个为64字节容量的StaticJsonDocument对象
    jsonData["id"] = "123";                                                                 // 消息ID号
    jsonData["params"]["step"] = String(nowProgress);                                       // OTA升级进度
    String data = "";                                                                       // 定义一个存放json对象转化成字符串的变量
    serializeJson(jsonData, data);                                                          // 将JSON对象转换成字符串，即反序列化
    uint16_t result = aliyunMqtt.sendCustomData(deviceReportFwUpgradeProgressTopic, data);  // 发送数据到云平台
#if debugState
    debugSerial.println("上报程序升级进度，发送数据为：[" + String(data) + "]，发送状态为：" + String(result ? "已发送，ID为：" + String(result) : "失败！"));
#endif
  }
}

/**
 * 函数功能：程序升级结束的回调函数
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void updateFinishedCallback() {
#if debugState
  debugSerial.println("程序升级已完成，将会自动重启设备。\r\n");
#endif
}

/**
 * 函数功能：程序升级失败的回调函数
 * 参数1：[_error] [int] 错误原因
 * 返回值：无
 * 注意事项：无
 */
void updateErrorCallback(int _error) {
#if debugState
  debugSerial.println("程序升级失败！原因为：" + String(_error));
#endif
}

/**
 * 函数功能：上报程序版本，连接MQTT服务器成功后只上报1次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void reportSoftwareVersion() {
  StaticJsonDocument<64> jsonData;                                                    // 实例化一个为64字节容量的StaticJsonDocument对象
  jsonData["id"] = "123";                                                             // 消息ID号
  jsonData["params"]["version"] = softwareVersion;                                    // OTA模块版本
  String data = "";                                                                   // 定义一个存放json对象转化成字符串的变量
  serializeJson(jsonData, data);                                                      // 将JSON对象转换成字符串，即反序列化
  uint16_t result = aliyunMqtt.sendCustomData(deviceReportFwUpgradeInfoTopic, data);  // 发送数据到云平台
#if debugState
  debugSerial.println("上报程序版本，发送数据为：[" + String(data) + "]，发送状态为：" + String(result ? "已发送，ID为：" + String(result) : "失败！"));
#endif
}

/**
 * 函数功能：固件升级响应回调
 * 参数1：[_data] [JsonVariant] JSON对象
 * 返回值：无
 * 注意事项：无
 */
void fwUpgradeCallback(JsonVariant _data) {
  String url = _data["data"]["url"];  // 获取升级包的下载连接
  if (url == "null") {                // 判断链接是否存在
    return;
  }
  url.replace("https://", "http://");  // 将https替换成http，简化操作
  fwDownloadUrl = url;                 // 升级包的下载连接
  fwUpgradeFlag = true;                // 固件升级的标志为true
}

/**
 * 函数功能：固件升级运行任务
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void fwUpgradeRunTask() {
  if (fwUpgradeFlag) {                                 // 如果固件升级的标志为true
    fwUpgradeFlag = false;                             // 固件升级的标志为false
    ESPhttpUpdate.onStart(updateStartedCallback);      // 设置程序升级开始的回调函数
    ESPhttpUpdate.onProgress(updateProgressCallback);  // 设置程序升级过程中的回调函数
    ESPhttpUpdate.onEnd(updateFinishedCallback);       // 设置程序升级结束的回调函数
    ESPhttpUpdate.onError(updateErrorCallback);        // 设置程序升级失败的回调函数

    WiFiClient httpClient;                                                      // 实例化一个httpClient
    t_httpUpdate_return ret = ESPhttpUpdate.update(httpClient, fwDownloadUrl);  // 开始升级
    switch (ret) {
      case HTTP_UPDATE_FAILED: {  // 当升级失败时
#if debugState
        debugSerial.println("httpUpdate升级失败！");
#endif
      } break;
      case HTTP_UPDATE_NO_UPDATES: {  // 当无升级时
#if debugState
        debugSerial.println("httpUpdate无升级");
#endif
      } break;
      case HTTP_UPDATE_OK: {  // 当升级成功时
#if debugState
        debugSerial.println("httpUpdate升级成功。");
#endif
      } break;
    }
  }
}

/**
 * 函数功能：MQTT连接事件的回调函数
 * 参数1：[_sessionPresent] [bool] 会话是否存在
 * 返回值：无
 * 注意事项：无
 */
void onMqttConnect(bool _sessionPresent) {
#if debugState
  debugSerial.println("阿里云物联网MQTT服务器已连接，会话存在状态为：" + String(_sessionPresent));
#endif
  nowMqttConnectState = MqttConnected;                                       // 当前MQTT的连接状态为已连接
  reportSoftwareVersion();                                                   // 上报程序版本，连接MQTT服务器成功后只上报1次
  aliyunMqtt.subscribeTopic(fwUpgradeInfoDownlinkTopic, fwUpgradeCallback);  // 订阅固件升级信息下行主题和设置响应回调
}

/**
 * 函数功能：MQTT断开连接事件的回调函数
 * 参数1：[_reason] [AsyncMqttClientDisconnectReason] MQTT断开连接的原因，在AsyncMqttClientDisconnectReason枚举中查看
 * 返回值：无
 * 注意事项：无
 */
void onMqttDisconnect(AsyncMqttClientDisconnectReason _reason) {
#if debugState
  debugSerial.println("阿里云物联网MQTT服务器已断开连接！原因为：" + String(uint8_t(_reason)));
#endif
  nowMqttConnectState = MqttDisconnected;  // 当前MQTT的连接状态为已断开连接
}

/**
 * 函数功能：检查MQTT和重连
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void checkMqttAndReconnect() {
  static uint8_t mqttConnectStateMachine = 0;  // MQTT连接的状态机
  static uint32_t nowTime = 0;                 // 检查时间
  switch (mqttConnectStateMachine) {
    // 检查MQTT的连接状态
    case 0: {
      if (MqttDisconnected == nowMqttConnectState) {  // 如果当前MQTT的连接状态为已断开连接
        aliyunMqtt.mqttClient.disconnect(true);       // 断开之前的连接
        aliyunMqtt.mqttClient.connect();              // 连接MQTT服务器，连接结果在连接事件的回调函数或断开连接事件的回调函数中处理
        mqttConnectStateMachine = 1;                  // MQTT连接中，状态机=1，重连中
        nowTime = millis();                           // 更新检查时间
#if debugState
        debugSerial.println("阿里云MQTT服务器已断开连接！开始尝试重连中......");
#endif
      }
    } break;
    // MQTT重连中
    case 1: {
      if (MqttConnected == nowMqttConnectState || millis() - nowTime > ckeckMqttConnectTime) {  // 如果当前MQTT的连接状态为已连接或者超过重连时间
        mqttConnectStateMachine = 0;                                                            // 状态机=0，回到检查中
      }
    } break;

    default:
      break;
  }
}

/**
 * 函数功能：程序初始化时调用，仅执行一次
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void setup() {
  delay(1000);  // 等待设备上电稳定

#if debugState
  debugSerial.begin(debugSerialBuadrate);  // 初始化调试打印的串口
  debugSerial.println("\r\n程序开始初始化......");
#endif

  WiFi.disconnect(true);                   // 断开当前WIFI连接
  WiFi.mode(WIFI_STA);                     // 设置WIFI模式为STA模式
  WiFi.begin(wifiName, wifiPassword);      // 开始连接WIFI
  while (WL_CONNECTED != WiFi.status()) {  // 判断WIFI的连接状态
    delay(500);
#if debugState
    debugSerial.print(".");
#endif
  }

  aliyunMqtt.setDebug(debugSerial);                                       // 设置是否开启aliyunMqtt库中的打印，传入串口即开启打印
  aliyunMqtt.setDeviceCertificate(productKey, deviceName, deviceSecret);  // 设置连接阿里云物联网平台的设备证书（也叫三元组）

  aliyunMqtt.mqttClient.setKeepAlive(mqttHeartbeatTime);    // 设置心跳间隔时间
  aliyunMqtt.mqttClient.setMaxTopicLength(mqttPacketSize);  // 设置MQTT数据包大小
  aliyunMqtt.mqttClient.onConnect(onMqttConnect);           // 设置MQTT连接事件的回调函数
  aliyunMqtt.mqttClient.onDisconnect(onMqttDisconnect);     // 设置MQTT断开连接事件的回调函数
  aliyunMqtt.mqttClient.connect();                          // 连接MQTT服务器，连接结果在连接事件的回调函数或断开连接事件的回调函数中处理
  nowMqttConnectState = MqttConnecting;                     // 当前MQTT的连接状态为连接中

#if debugState
  debugSerial.println("\r\n程序初始化完成！");
  debugSerial.println("当前程序版本为：" + String(softwareVersion));
#endif
}

/**
 * 函数功能：程序主循环函数，重复运行
 * 参数：无
 * 返回值：无
 * 注意事项：无
 */
void loop() {
  checkMqttAndReconnect();  // 检查MQTT和重连
  fwUpgradeRunTask();       // 固件升级运行任务
}
